/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2005 - 2013 Red Hat, Inc.
 * Copyright (C) 2006 - 2008 Novell, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-vpn-connection.h"

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <syslog.h>
#include <linux/rtnetlink.h>

#include "nm-proxy-config.h"
#include "nm-ip4-config.h"
#include "nm-ip6-config.h"
#include "libnm-platform/nm-platform.h"
#include "nm-active-connection.h"
#include "NetworkManagerUtils.h"
#include "settings/nm-settings-connection.h"
#include "nm-dispatcher.h"
#include "nm-netns.h"
#include "settings/nm-agent-manager.h"
#include "libnm-core-intern/nm-core-internal.h"
#include "nm-pacrunner-manager.h"
#include "nm-firewall-manager.h"
#include "nm-config.h"
#include "nm-vpn-plugin-info.h"
#include "nm-vpn-manager.h"
#include "dns/nm-dns-manager.h"

typedef enum {
    /* Only system secrets */
    SECRETS_REQ_SYSTEM = 0,
    /* All existing secrets including agent secrets */
    SECRETS_REQ_EXISTING = 1,
    /* New secrets required; ask an agent */
    SECRETS_REQ_NEW = 2,
    /* Plugin requests secrets interactively */
    SECRETS_REQ_INTERACTIVE = 3,
    /* Placeholder for bounds checking */
    SECRETS_REQ_LAST
} SecretsReq;

/* Internal VPN states, private to NMVpnConnection */
typedef enum {
    STATE_UNKNOWN = 0,
    STATE_WAITING,
    STATE_PREPARE,
    STATE_NEED_AUTH,
    STATE_CONNECT,
    STATE_IP_CONFIG_GET,
    STATE_PRE_UP,
    STATE_ACTIVATED,
    STATE_DEACTIVATING,
    STATE_DISCONNECTED,
    STATE_FAILED,
} VpnState;

enum {
    INTERNAL_STATE_CHANGED,
    INTERNAL_RETRY_AFTER_FAILURE,

    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = {0};

NM_GOBJECT_PROPERTIES_DEFINE(NMVpnConnection, PROP_VPN_STATE, PROP_BANNER,
#define PROP_IP4_CONFIG 2000
#define PROP_IP6_CONFIG 2001
#define PROP_MASTER     2002
);

typedef struct {
    gboolean service_can_persist;
    gboolean connection_can_persist;

    NMSettingsConnectionCallId *secrets_id;
    SecretsReq                  secrets_idx;
    char *                      username;

    VpnState                      vpn_state;
    NMDispatcherCallId *          dispatcher_id;
    NMActiveConnectionStateReason failure_reason;

    NMVpnServiceState service_state;
    guint             start_timeout;
    gboolean          service_running;
    NMVpnPluginInfo * plugin_info;
    char *            bus_name;

    NMFirewallManagerCallId *fw_call;

    NMNetns *netns;

    GPtrArray *ip4_dev_route_blacklist;

    GDBusProxy *       proxy;
    GCancellable *     cancellable;
    GVariant *         connect_hash;
    guint              connect_timeout;
    NMProxyConfig *    proxy_config;
    NMPacrunnerConfId *pacrunner_conf_id;
    gboolean           has_ip4;
    NMIP4Config *      ip4_config;
    guint32            ip4_internal_gw;
    guint32            ip4_external_gw;
    gboolean           has_ip6;
    NMIP6Config *      ip6_config;

    /* These config instances are passed on to NMDevice and modified by NMDevice.
     * This pointer is only useful for nm_device_replace_vpn4_config() to clear the
     * previous configuration. Consider these instances to be owned by NMDevice. */
    NMIP4Config *last_device_ip4_config;
    NMIP6Config *last_device_ip6_config;

    struct in6_addr *ip6_internal_gw;
    struct in6_addr *ip6_external_gw;
    char *           ip_iface;
    int              ip_ifindex;
    char *           banner;
    guint32          mtu;
} NMVpnConnectionPrivate;

struct _NMVpnConnection {
    NMActiveConnection     parent;
    NMVpnConnectionPrivate _priv;
};

struct _NMVpnConnectionClass {
    NMActiveConnectionClass parent;
};

G_DEFINE_TYPE(NMVpnConnection, nm_vpn_connection, NM_TYPE_ACTIVE_CONNECTION)

#define NM_VPN_CONNECTION_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMVpnConnection, NM_IS_VPN_CONNECTION, NMActiveConnection)

/*****************************************************************************/

static const NMDBusInterfaceInfoExtended interface_info_vpn_connection;
static const GDBusSignalInfo             signal_info_vpn_state_changed;

static NMSettingsConnection *_get_settings_connection(NMVpnConnection *self,
                                                      gboolean         allow_missing);

static void get_secrets(NMVpnConnection *self, SecretsReq secrets_idx, const char *const *hints);

static guint32 get_route_table(NMVpnConnection *self, int addr_family, gboolean fallback_main);

static void plugin_interactive_secrets_required(NMVpnConnection *  self,
                                                const char *       message,
                                                const char *const *secrets);

static void _set_vpn_state(NMVpnConnection *             self,
                           VpnState                      vpn_state,
                           NMActiveConnectionStateReason reason,
                           gboolean                      quitting);

/*****************************************************************************/

#define _NMLOG_DOMAIN      LOGD_VPN
#define _NMLOG_PREFIX_NAME "vpn-connection"

#define __NMLOG_prefix_buf_len 128

static const char *
__LOG_create_prefix(char *buf, NMVpnConnection *self, NMSettingsConnection *con)
{
    NMVpnConnectionPrivate *priv;
    const char *            id;

    if (!self)
        return _NMLOG_PREFIX_NAME;

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    id   = con ? nm_settings_connection_get_id(con) : NULL;

    g_snprintf(buf,
               __NMLOG_prefix_buf_len,
               "%s["
               "%p"       /*self*/
               "%s%s"     /*con-uuid*/
               "%s%s%s%s" /*con-id*/
               ",%d"      /*ifindex*/
               "%s%s%s"   /*iface*/
               "]",
               _NMLOG_PREFIX_NAME,
               self,
               con ? "," : "--",
               con ? (nm_settings_connection_get_uuid(con) ?: "??") : "",
               con ? "," : "",
               NM_PRINT_FMT_QUOTED(id, "\"", id, "\"", con ? "??" : ""),
               priv->ip_ifindex,
               NM_PRINT_FMT_QUOTED(priv->ip_iface, ":(", priv->ip_iface, ")", ""));

    return buf;
}

#define _NMLOG(level, ...)                                                                   \
    G_STMT_START                                                                             \
    {                                                                                        \
        const NMLogLevel      _level = (level);                                              \
        NMSettingsConnection *_con   = (self) ? _get_settings_connection(self, TRUE) : NULL; \
                                                                                             \
        if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {                                     \
            char __prefix[__NMLOG_prefix_buf_len];                                           \
                                                                                             \
            _nm_log(_level,                                                                  \
                    _NMLOG_DOMAIN,                                                           \
                    0,                                                                       \
                    (self) ? NM_VPN_CONNECTION_GET_PRIVATE(self)->ip_iface : NULL,           \
                    (_con) ? nm_settings_connection_get_uuid(_con) : NULL,                   \
                    "%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                               \
                    __LOG_create_prefix(__prefix, (self), _con)                              \
                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                                  \
        }                                                                                    \
    }                                                                                        \
    G_STMT_END

/*****************************************************************************/

static void
cancel_get_secrets(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->secrets_id) {
        nm_settings_connection_cancel_secrets(_get_settings_connection(self, FALSE),
                                              priv->secrets_id);
        g_warn_if_fail(!priv->secrets_id);
        priv->secrets_id = NULL;
    }
}

static NMVpnConnectionState
_state_to_nm_vpn_state(VpnState state)
{
    switch (state) {
    case STATE_WAITING:
    case STATE_PREPARE:
        return NM_VPN_CONNECTION_STATE_PREPARE;
    case STATE_NEED_AUTH:
        return NM_VPN_CONNECTION_STATE_NEED_AUTH;
    case STATE_CONNECT:
        return NM_VPN_CONNECTION_STATE_CONNECT;
    case STATE_IP_CONFIG_GET:
    case STATE_PRE_UP:
        return NM_VPN_CONNECTION_STATE_IP_CONFIG_GET;
    case STATE_ACTIVATED:
        return NM_VPN_CONNECTION_STATE_ACTIVATED;
    case STATE_DEACTIVATING:
    {
        /* Map DEACTIVATING to ACTIVATED to preserve external API behavior,
         * since our API has no DEACTIVATING state of its own.  Since this can
         * take some time, and the VPN isn't actually disconnected until it
         * hits the DISCONNECTED state, to clients it should still appear
         * connected.
         */
        return NM_VPN_CONNECTION_STATE_ACTIVATED;
    }
    case STATE_DISCONNECTED:
        return NM_VPN_CONNECTION_STATE_DISCONNECTED;
    case STATE_FAILED:
        return NM_VPN_CONNECTION_STATE_FAILED;
    default:
        return NM_VPN_CONNECTION_STATE_UNKNOWN;
    }
}

static NMActiveConnectionState
_state_to_ac_state(VpnState vpn_state)
{
    /* Set the NMActiveConnection state based on VPN state */
    switch (vpn_state) {
    case STATE_WAITING:
    case STATE_PREPARE:
    case STATE_NEED_AUTH:
    case STATE_CONNECT:
    case STATE_IP_CONFIG_GET:
    case STATE_PRE_UP:
        return NM_ACTIVE_CONNECTION_STATE_ACTIVATING;
    case STATE_ACTIVATED:
        return NM_ACTIVE_CONNECTION_STATE_ACTIVATED;
    case STATE_DEACTIVATING:
        return NM_ACTIVE_CONNECTION_STATE_DEACTIVATING;
    case STATE_DISCONNECTED:
    case STATE_FAILED:
        return NM_ACTIVE_CONNECTION_STATE_DEACTIVATED;
    default:
        break;
    }
    return NM_ACTIVE_CONNECTION_STATE_UNKNOWN;
}

static NMSettingsConnection *
_get_settings_connection(NMVpnConnection *self, gboolean allow_missing)
{
    NMSettingsConnection *con;

    /* Currently, we operate on the assumption, that the settings-connection
     * never changes after it is set (though initially, it might be unset).
     * Later we might want to change that, but then we need fixes here too. */

    con = _nm_active_connection_get_settings_connection(NM_ACTIVE_CONNECTION(self));
    if (!con && !allow_missing)
        g_return_val_if_reached(NULL);
    return con;
}

static NMConnection *
_get_applied_connection(NMVpnConnection *connection)
{
    NMConnection *con;

    con = nm_active_connection_get_applied_connection(NM_ACTIVE_CONNECTION(connection));
    g_return_val_if_fail(con, NULL);
    return con;
}

static void
disconnect_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
{
    GVariant *variant;

    variant = g_dbus_proxy_call_finish(proxy, result, NULL);
    if (variant)
        g_variant_unref(variant);
    g_object_unref(user_data);
}

static void
fw_call_cleanup(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->fw_call) {
        nm_firewall_manager_cancel_call(priv->fw_call);
        g_warn_if_fail(!priv->fw_call);
        priv->fw_call = NULL;
    }
}

static void
remove_parent_device_config(NMVpnConnection *connection, NMDevice *device)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(connection);

    if (priv->last_device_ip4_config) {
        nm_device_replace_vpn4_config(device, priv->last_device_ip4_config, NULL);
        g_clear_object(&priv->last_device_ip4_config);
    }

    if (priv->last_device_ip6_config) {
        nm_device_replace_vpn6_config(device, priv->last_device_ip6_config, NULL);
        g_clear_object(&priv->last_device_ip6_config);
    }
}

static void
vpn_cleanup(NMVpnConnection *self, NMDevice *parent_dev)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->ip_ifindex) {
        NMPlatform *platform = nm_netns_get_platform(priv->netns);

        nm_platform_link_set_down(platform, priv->ip_ifindex);
        nm_platform_ip_route_flush(platform, AF_UNSPEC, priv->ip_ifindex);
        nm_platform_ip_address_flush(platform, AF_UNSPEC, priv->ip_ifindex);
    }

    remove_parent_device_config(self, parent_dev);

    /* Remove zone from firewall */
    if (priv->ip_iface) {
        nm_firewall_manager_remove_from_zone(nm_firewall_manager_get(),
                                             priv->ip_iface,
                                             NULL,
                                             NULL,
                                             NULL);
    }
    /* Cancel pending firewall call */
    fw_call_cleanup(self);

    g_free(priv->banner);
    priv->banner = NULL;

    g_free(priv->ip_iface);
    priv->ip_iface   = NULL;
    priv->ip_ifindex = 0;

    g_free(priv->bus_name);
    priv->bus_name = NULL;

    /* Clear out connection secrets to ensure that the settings service
     * gets asked for them next time the connection is activated.
     */
    nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(self));
}

static void
dispatcher_pre_down_done(NMDispatcherCallId *call_id, gpointer user_data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_assert(call_id);
    nm_assert(priv->dispatcher_id == call_id);

    priv->dispatcher_id = NULL;
    _set_vpn_state(self,
                   STATE_DISCONNECTED,
                   NM_ACTIVE_CONNECTION_STATE_REASON_USER_DISCONNECTED,
                   FALSE);
}

static void
dispatcher_pre_up_done(NMDispatcherCallId *call_id, gpointer user_data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_assert(call_id);
    nm_assert(priv->dispatcher_id == call_id);

    priv->dispatcher_id = NULL;
    _set_vpn_state(self, STATE_ACTIVATED, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
}

static void
dispatcher_cleanup(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->dispatcher_id)
        nm_dispatcher_call_cancel(g_steal_pointer(&priv->dispatcher_id));
}

static void
_set_vpn_state(NMVpnConnection *             self,
               VpnState                      vpn_state,
               NMActiveConnectionStateReason reason,
               gboolean                      quitting)
{
    NMVpnConnectionPrivate *priv;
    VpnState                old_vpn_state;
    NMVpnConnectionState    new_external_state, old_external_state;
    NMDevice *    parent_dev = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(self));
    NMConnection *applied;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (vpn_state == priv->vpn_state)
        return;

    old_vpn_state   = priv->vpn_state;
    priv->vpn_state = vpn_state;

    /* The device gets destroyed by active connection when it enters
     * the deactivated state, so we need to ref it for usage below.
     */
    if (parent_dev)
        g_object_ref(parent_dev);

    /* Update active connection base class state */
    nm_active_connection_set_state(NM_ACTIVE_CONNECTION(self),
                                   _state_to_ac_state(vpn_state),
                                   reason);

    /* Clear any in-progress secrets request */
    cancel_get_secrets(self);

    dispatcher_cleanup(self);

    /* The connection gets destroyed by the VPN manager when it enters the
     * disconnected/failed state, but we need to keep it around for a bit
     * to send out signals and handle the dispatcher.  So ref it.
     */
    g_object_ref(self);

    old_external_state = _state_to_nm_vpn_state(old_vpn_state);
    new_external_state = _state_to_nm_vpn_state(priv->vpn_state);
    if (new_external_state != old_external_state) {
        nm_dbus_object_emit_signal(NM_DBUS_OBJECT(self),
                                   &interface_info_vpn_connection,
                                   &signal_info_vpn_state_changed,
                                   "(uu)",
                                   (guint32) new_external_state,
                                   (guint32) reason);
        g_signal_emit(self,
                      signals[INTERNAL_STATE_CHANGED],
                      0,
                      new_external_state,
                      old_external_state,
                      reason);
        _notify(self, PROP_VPN_STATE);
    }

    switch (vpn_state) {
    case STATE_NEED_AUTH:
        /* Do nothing; not part of 'default' because we don't want to touch
         * priv->secrets_req as NEED_AUTH is re-entered during interactive
         * secrets.
         */
        break;
    case STATE_PRE_UP:
        if (!nm_dispatcher_call_vpn(NM_DISPATCHER_ACTION_VPN_PRE_UP,
                                    _get_settings_connection(self, FALSE),
                                    _get_applied_connection(self),
                                    parent_dev,
                                    priv->ip_iface,
                                    priv->proxy_config,
                                    priv->ip4_config,
                                    priv->ip6_config,
                                    dispatcher_pre_up_done,
                                    self,
                                    &priv->dispatcher_id)) {
            /* Just proceed on errors */
            dispatcher_pre_up_done(0, self);
        }
        break;
    case STATE_ACTIVATED:
        applied = _get_applied_connection(self);

        /* Secrets no longer needed now that we're connected */
        nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(self));

        /* Let dispatcher scripts know we're up and running */
        nm_dispatcher_call_vpn(NM_DISPATCHER_ACTION_VPN_UP,
                               _get_settings_connection(self, FALSE),
                               applied,
                               parent_dev,
                               priv->ip_iface,
                               priv->proxy_config,
                               priv->ip4_config,
                               priv->ip6_config,
                               NULL,
                               NULL,
                               NULL);

        if (priv->proxy_config) {
            nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id);
            priv->pacrunner_conf_id = nm_pacrunner_manager_add(nm_pacrunner_manager_get(),
                                                               priv->proxy_config,
                                                               priv->ip_iface,
                                                               priv->ip4_config,
                                                               priv->ip6_config);
        }
        break;
    case STATE_DEACTIVATING:
        applied = _get_applied_connection(self);
        if (quitting) {
            nm_dispatcher_call_vpn_sync(NM_DISPATCHER_ACTION_VPN_PRE_DOWN,
                                        _get_settings_connection(self, FALSE),
                                        applied,
                                        parent_dev,
                                        priv->ip_iface,
                                        priv->proxy_config,
                                        priv->ip4_config,
                                        priv->ip6_config);
        } else {
            if (!nm_dispatcher_call_vpn(NM_DISPATCHER_ACTION_VPN_PRE_DOWN,
                                        _get_settings_connection(self, FALSE),
                                        applied,
                                        parent_dev,
                                        priv->ip_iface,
                                        priv->proxy_config,
                                        priv->ip4_config,
                                        priv->ip6_config,
                                        dispatcher_pre_down_done,
                                        self,
                                        &priv->dispatcher_id)) {
                /* Just proceed on errors */
                dispatcher_pre_down_done(0, self);
            }
        }

        nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id);
        break;
    case STATE_FAILED:
    case STATE_DISCONNECTED:
        if (old_vpn_state >= STATE_ACTIVATED && old_vpn_state <= STATE_DEACTIVATING) {
            /* Let dispatcher scripts know we're about to go down */
            if (quitting) {
                nm_dispatcher_call_vpn_sync(NM_DISPATCHER_ACTION_VPN_DOWN,
                                            _get_settings_connection(self, FALSE),
                                            _get_applied_connection(self),
                                            parent_dev,
                                            priv->ip_iface,
                                            NULL,
                                            NULL,
                                            NULL);
            } else {
                nm_dispatcher_call_vpn(NM_DISPATCHER_ACTION_VPN_DOWN,
                                       _get_settings_connection(self, FALSE),
                                       _get_applied_connection(self),
                                       parent_dev,
                                       priv->ip_iface,
                                       NULL,
                                       NULL,
                                       NULL,
                                       NULL,
                                       NULL,
                                       NULL);
            }
        }

        /* Tear down and clean up the connection */
        if (priv->proxy) {
            g_dbus_proxy_call(priv->proxy,
                              "Disconnect",
                              NULL,
                              G_DBUS_CALL_FLAGS_NONE,
                              -1,
                              priv->cancellable,
                              (GAsyncReadyCallback) disconnect_cb,
                              g_object_ref(self));
        }

        vpn_cleanup(self, parent_dev);
        /* fall-through */
    default:
        priv->secrets_idx = SECRETS_REQ_SYSTEM;
        break;
    }

    g_object_unref(self);
    if (parent_dev)
        g_object_unref(parent_dev);
}

static gboolean
_service_and_connection_can_persist(NMVpnConnection *self)
{
    return NM_VPN_CONNECTION_GET_PRIVATE(self)->connection_can_persist
           && NM_VPN_CONNECTION_GET_PRIVATE(self)->service_can_persist;
}

static gboolean
_connection_only_can_persist(NMVpnConnection *self)
{
    return NM_VPN_CONNECTION_GET_PRIVATE(self)->connection_can_persist
           && !NM_VPN_CONNECTION_GET_PRIVATE(self)->service_can_persist;
}

static void
device_state_changed(NMActiveConnection *active,
                     NMDevice *          device,
                     NMDeviceState       new_state,
                     NMDeviceState       old_state)
{
    if (_service_and_connection_can_persist(NM_VPN_CONNECTION(active))) {
        if (new_state <= NM_DEVICE_STATE_DISCONNECTED || new_state == NM_DEVICE_STATE_FAILED) {
            nm_active_connection_set_device(active, NULL);
        }
        return;
    }

    if (new_state <= NM_DEVICE_STATE_DISCONNECTED) {
        _set_vpn_state(NM_VPN_CONNECTION(active),
                       STATE_DISCONNECTED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED,
                       FALSE);
    } else if (new_state == NM_DEVICE_STATE_FAILED) {
        _set_vpn_state(NM_VPN_CONNECTION(active),
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_DEVICE_DISCONNECTED,
                       FALSE);
    }

    /* FIXME: map device DEACTIVATING state to VPN DEACTIVATING state and
     * block device deactivation on VPN deactivation.
     */
}

static void
add_ip4_vpn_gateway_route(NMIP4Config *config,
                          NMDevice *   parent_device,
                          in_addr_t    vpn_gw,
                          NMPlatform * platform)
{
    guint32              parent_gw     = 0;
    gboolean             has_parent_gw = FALSE;
    NMPlatformIP4Route   route;
    int                  ifindex;
    guint32              route_metric;
    nm_auto_nmpobj const NMPObject *route_resolved = NULL;

    g_return_if_fail(NM_IS_IP4_CONFIG(config));
    g_return_if_fail(NM_IS_DEVICE(parent_device));
    g_return_if_fail(vpn_gw != 0);

    ifindex = nm_ip4_config_get_ifindex(config);

    nm_assert(ifindex > 0);
    nm_assert(ifindex == nm_device_get_ip_ifindex(parent_device));

    /* Ask kernel how to reach @vpn_gw. We can only inject the route in
     * @parent_device, so whatever we resolve, it can only be on @ifindex. */
    if (nm_platform_ip_route_get(platform,
                                 AF_INET,
                                 &vpn_gw,
                                 ifindex,
                                 (NMPObject **) &route_resolved)
        >= 0) {
        const NMPlatformIP4Route *r = NMP_OBJECT_CAST_IP4_ROUTE(route_resolved);

        if (r->ifindex == ifindex) {
            const NMPObject *obj;

            /* `ip route get` always resolves the route, even if the destination is unreachable.
             * In which case, it pretends the destination is directly reachable.
             *
             * So, only accept direct routes if @vpn_gw is a private network
             * or if the parent device also has a direct default route */
            if (nm_platform_route_table_is_main(r->table_coerced)) {
                if (r->gateway) {
                    parent_gw     = r->gateway;
                    has_parent_gw = TRUE;
                } else if (nm_utils_ip_is_site_local(AF_INET, &vpn_gw)) {
                    has_parent_gw = TRUE;
                } else if ((obj = nm_device_get_best_default_route(parent_device, AF_INET))
                           && !NMP_OBJECT_CAST_IP4_ROUTE(obj)->gateway) {
                    has_parent_gw = TRUE;
                }
            }
        }
    }

    if (!has_parent_gw)
        return;

    route_metric = nm_device_get_route_metric(parent_device, AF_INET);

    memset(&route, 0, sizeof(route));
    route.ifindex   = ifindex;
    route.network   = vpn_gw;
    route.plen      = 32;
    route.gateway   = parent_gw;
    route.rt_source = NM_IP_CONFIG_SOURCE_VPN;
    route.metric    = route_metric;
    nm_ip4_config_add_route(config, &route, NULL);

    if (parent_gw) {
        /* Ensure there's a route to the parent device's gateway through the
         * parent device, since if the VPN claims the default route and the VPN
         * routes include a subnet that matches the parent device's subnet,
         * the parent device's gateway would get routed through the VPN and fail.
         */
        memset(&route, 0, sizeof(route));
        route.network   = parent_gw;
        route.plen      = 32;
        route.rt_source = NM_IP_CONFIG_SOURCE_VPN;
        route.metric    = route_metric;
        nm_ip4_config_add_route(config, &route, NULL);
    }
}

static void
add_ip6_vpn_gateway_route(NMIP6Config *          config,
                          NMDevice *             parent_device,
                          const struct in6_addr *vpn_gw,
                          NMPlatform *           platform)
{
    const struct in6_addr *parent_gw     = NULL;
    gboolean               has_parent_gw = FALSE;
    NMPlatformIP6Route     route;
    int                    ifindex;
    guint32                route_metric;
    nm_auto_nmpobj const NMPObject *route_resolved = NULL;

    g_return_if_fail(NM_IS_IP6_CONFIG(config));
    g_return_if_fail(NM_IS_DEVICE(parent_device));
    g_return_if_fail(vpn_gw != NULL);

    ifindex = nm_ip6_config_get_ifindex(config);

    nm_assert(ifindex > 0);
    nm_assert(ifindex == nm_device_get_ip_ifindex(parent_device));

    /* Ask kernel how to reach @vpn_gw. We can only inject the route in
     * @parent_device, so whatever we resolve, it can only be on @ifindex. */
    if (nm_platform_ip_route_get(platform,
                                 AF_INET6,
                                 vpn_gw,
                                 ifindex,
                                 (NMPObject **) &route_resolved)
        >= 0) {
        const NMPlatformIP6Route *r = NMP_OBJECT_CAST_IP6_ROUTE(route_resolved);

        if (r->ifindex == ifindex) {
            const NMPObject *obj;

            /* `ip route get` always resolves the route, even if the destination is unreachable.
             * In which case, it pretends the destination is directly reachable.
             *
             * So, only accept direct routes if @vpn_gw is a private network
             * or if the parent device also has a direct default route */
            if (nm_platform_route_table_is_main(r->table_coerced)) {
                if (!IN6_IS_ADDR_UNSPECIFIED(&r->gateway)) {
                    parent_gw     = &r->gateway;
                    has_parent_gw = TRUE;
                } else if (nm_utils_ip_is_site_local(AF_INET6, &vpn_gw)) {
                    has_parent_gw = TRUE;
                } else if ((obj = nm_device_get_best_default_route(parent_device, AF_INET6))
                           && IN6_IS_ADDR_UNSPECIFIED(&NMP_OBJECT_CAST_IP6_ROUTE(obj)->gateway)) {
                    has_parent_gw = TRUE;
                }
            }
        }
    }

    if (!has_parent_gw)
        return;

    route_metric = nm_device_get_route_metric(parent_device, AF_INET6);

    memset(&route, 0, sizeof(route));
    route.ifindex = ifindex;
    route.network = *vpn_gw;
    route.plen    = 128;
    if (parent_gw)
        route.gateway = *parent_gw;
    route.rt_source = NM_IP_CONFIG_SOURCE_VPN;
    route.metric    = route_metric;
    nm_ip6_config_add_route(config, &route, NULL);

    /* Ensure there's a route to the parent device's gateway through the
     * parent device, since if the VPN claims the default route and the VPN
     * routes include a subnet that matches the parent device's subnet,
     * the parent device's gateway would get routed through the VPN and fail.
     */
    if (parent_gw && !IN6_IS_ADDR_UNSPECIFIED(parent_gw)) {
        memset(&route, 0, sizeof(route));
        route.network   = *parent_gw;
        route.plen      = 128;
        route.rt_source = NM_IP_CONFIG_SOURCE_VPN;
        route.metric    = route_metric;
        nm_ip6_config_add_route(config, &route, NULL);
    }
}

NMVpnConnection *
nm_vpn_connection_new(NMSettingsConnection * settings_connection,
                      NMDevice *             parent_device,
                      const char *           specific_object,
                      NMActivationReason     activation_reason,
                      NMActivationStateFlags initial_state_flags,
                      NMAuthSubject *        subject)
{
    g_return_val_if_fail(!settings_connection || NM_IS_SETTINGS_CONNECTION(settings_connection),
                         NULL);
    g_return_val_if_fail(NM_IS_DEVICE(parent_device), NULL);
    g_return_val_if_fail(specific_object, NULL);

    return g_object_new(NM_TYPE_VPN_CONNECTION,
                        NM_ACTIVE_CONNECTION_INT_SETTINGS_CONNECTION,
                        settings_connection,
                        NM_ACTIVE_CONNECTION_INT_DEVICE,
                        parent_device,
                        NM_ACTIVE_CONNECTION_SPECIFIC_OBJECT,
                        specific_object,
                        NM_ACTIVE_CONNECTION_INT_SUBJECT,
                        subject,
                        NM_ACTIVE_CONNECTION_INT_ACTIVATION_REASON,
                        activation_reason,
                        NM_ACTIVE_CONNECTION_VPN,
                        TRUE,
                        NM_ACTIVE_CONNECTION_STATE_FLAGS,
                        (guint) initial_state_flags,
                        NULL);
}

const char *
nm_vpn_connection_get_service(NMVpnConnection *self)
{
    NMSettingVpn *s_vpn;

    s_vpn = nm_connection_get_setting_vpn(_get_applied_connection(self));
    return nm_setting_vpn_get_service_type(s_vpn);
}

static NM_UTILS_LOOKUP_STR_DEFINE(
    _vpn_plugin_failure_to_string,
    NMVpnPluginFailure,
    NM_UTILS_LOOKUP_DEFAULT(NULL),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_PLUGIN_FAILURE_LOGIN_FAILED, "login-failed"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_PLUGIN_FAILURE_CONNECT_FAILED, "connect-failed"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_PLUGIN_FAILURE_BAD_IP_CONFIG, "bad-ip-config"), );

#define vpn_plugin_failure_to_string_a(failure) \
    NM_UTILS_LOOKUP_STR_A(_vpn_plugin_failure_to_string, failure)

static void
plugin_failed(NMVpnConnection *self, guint reason)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    _LOGW("VPN plugin: failed: %s (%d)", vpn_plugin_failure_to_string_a(reason), reason);

    switch (reason) {
    case NM_VPN_PLUGIN_FAILURE_LOGIN_FAILED:
        priv->failure_reason = NM_ACTIVE_CONNECTION_STATE_REASON_LOGIN_FAILED;
        break;
    case NM_VPN_PLUGIN_FAILURE_BAD_IP_CONFIG:
        priv->failure_reason = NM_ACTIVE_CONNECTION_STATE_REASON_IP_CONFIG_INVALID;
        break;
    default:
        priv->failure_reason = NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN;
        break;
    }
}

static NM_UTILS_LOOKUP_STR_DEFINE(
    _vpn_service_state_to_string,
    NMVpnServiceState,
    NM_UTILS_LOOKUP_DEFAULT(NULL),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_UNKNOWN, "unknown"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_INIT, "init"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_SHUTDOWN, "shutdown"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_STARTING, "starting"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_STARTED, "started"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_STOPPING, "stopping"),
    NM_UTILS_LOOKUP_STR_ITEM(NM_VPN_SERVICE_STATE_STOPPED, "stopped"), );

#define vpn_service_state_to_string_a(state) \
    NM_UTILS_LOOKUP_STR_A(_vpn_service_state_to_string, state)

static NM_UTILS_LOOKUP_STR_DEFINE(_vpn_state_to_string,
                                  VpnState,
                                  NM_UTILS_LOOKUP_DEFAULT(NULL),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_UNKNOWN, "unknown"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_WAITING, "waiting"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_PREPARE, "prepare"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_NEED_AUTH, "need-auth"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_CONNECT, "connect"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_IP_CONFIG_GET, "ip-config-get"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_PRE_UP, "pre-up"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_ACTIVATED, "activated"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_DEACTIVATING, "deactivating"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_DISCONNECTED, "disconnected"),
                                  NM_UTILS_LOOKUP_STR_ITEM(STATE_FAILED, "failed"), );

#define vpn_state_to_string_a(state) NM_UTILS_LOOKUP_STR_A(_vpn_state_to_string, state)

static void
plugin_state_changed(NMVpnConnection *self, NMVpnServiceState new_service_state)
{
    NMVpnConnectionPrivate *priv              = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMVpnServiceState       old_service_state = priv->service_state;

    _LOGI("VPN plugin: state changed: %s (%d)",
          vpn_service_state_to_string_a(new_service_state),
          new_service_state);
    priv->service_state = new_service_state;

    if (new_service_state == NM_VPN_SERVICE_STATE_STOPPED) {
        /* Clear connection secrets to ensure secrets get requested each time the
         * connection is activated.
         */
        nm_active_connection_clear_secrets(NM_ACTIVE_CONNECTION(self));

        if ((priv->vpn_state >= STATE_WAITING) && (priv->vpn_state <= STATE_ACTIVATED)) {
            VpnState old_state = priv->vpn_state;

            _set_vpn_state(self, STATE_FAILED, priv->failure_reason, FALSE);

            /* Reset the failure reason */
            priv->failure_reason = NM_ACTIVE_CONNECTION_STATE_REASON_UNKNOWN;

            /* If the connection failed, the service cannot persist, but the
             * connection can persist, ask listeners to re-activate the connection.
             */
            if (old_state == STATE_ACTIVATED && priv->vpn_state == STATE_FAILED
                && _connection_only_can_persist(self))
                g_signal_emit(self, signals[INTERNAL_RETRY_AFTER_FAILURE], 0);
        }
    } else if (new_service_state == NM_VPN_SERVICE_STATE_STARTING
               && old_service_state == NM_VPN_SERVICE_STATE_STARTED) {
        /* The VPN service got disconnected and is attempting to reconnect */
        _set_vpn_state(self,
                       STATE_CONNECT,
                       NM_ACTIVE_CONNECTION_STATE_REASON_CONNECT_TIMEOUT,
                       FALSE);
    }
}

static void
print_vpn_config(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    const NMPlatformIP4Address *address4;
    const NMPlatformIP6Address *address6;
    char *                      dns_domain = NULL;
    guint32                     num, i;
    char                        b1[NM_UTILS_INET_ADDRSTRLEN];
    char                        b2[NM_UTILS_INET_ADDRSTRLEN];
    NMDedupMultiIter            ipconf_iter;

    if (priv->ip4_external_gw) {
        _LOGI("Data: VPN Gateway: %s", _nm_utils_inet4_ntop(priv->ip4_external_gw, b1));
    } else if (priv->ip6_external_gw) {
        _LOGI("Data: VPN Gateway: %s", _nm_utils_inet6_ntop(priv->ip6_external_gw, b1));
    }

    _LOGI("Data: Tunnel Device: %s%s%s", NM_PRINT_FMT_QUOTE_STRING(priv->ip_iface));

    if (priv->ip4_config) {
        const NMPlatformIP4Route *route;

        _LOGI("Data: IPv4 configuration:");

        address4 = nm_ip4_config_get_first_address(priv->ip4_config);
        nm_assert(address4);

        if (priv->ip4_internal_gw)
            _LOGI("Data:   Internal Gateway: %s", _nm_utils_inet4_ntop(priv->ip4_internal_gw, b1));
        _LOGI("Data:   Internal Address: %s",
              address4 ? _nm_utils_inet4_ntop(address4->address, b1) : "??");
        _LOGI("Data:   Internal Prefix: %d", address4 ? (int) address4->plen : -1);
        _LOGI("Data:   Internal Point-to-Point Address: %s",
              _nm_utils_inet4_ntop(address4->peer_address, b1));

        nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, priv->ip4_config, &route) {
            _LOGI("Data:   Static Route: %s/%d   Next Hop: %s",
                  _nm_utils_inet4_ntop(route->network, b1),
                  route->plen,
                  _nm_utils_inet4_ntop(route->gateway, b2));
        }

        num = nm_ip4_config_get_num_nameservers(priv->ip4_config);
        for (i = 0; i < num; i++) {
            _LOGI("Data:   Internal DNS: %s",
                  _nm_utils_inet4_ntop(nm_ip4_config_get_nameserver(priv->ip4_config, i), b1));
        }

        if (nm_ip4_config_get_num_domains(priv->ip4_config) > 0)
            dns_domain = (char *) nm_ip4_config_get_domain(priv->ip4_config, 0);

        _LOGI("Data:   DNS Domain: '%s'", dns_domain ?: "(none)");
    } else
        _LOGI("Data: No IPv4 configuration");

    if (priv->ip6_config) {
        const NMPlatformIP6Route *route;

        _LOGI("Data: IPv6 configuration:");

        address6 = nm_ip6_config_get_first_address(priv->ip6_config);
        nm_assert(address6);

        if (priv->ip6_internal_gw)
            _LOGI("Data:   Internal Gateway: %s", _nm_utils_inet6_ntop(priv->ip6_internal_gw, b1));
        _LOGI("Data:   Internal Address: %s", _nm_utils_inet6_ntop(&address6->address, b1));
        _LOGI("Data:   Internal Prefix: %d", address6->plen);
        _LOGI("Data:   Internal Point-to-Point Address: %s",
              _nm_utils_inet6_ntop(&address6->peer_address, b1));

        nm_ip_config_iter_ip6_route_for_each (&ipconf_iter, priv->ip6_config, &route) {
            _LOGI("Data:   Static Route: %s/%d   Next Hop: %s",
                  _nm_utils_inet6_ntop(&route->network, b1),
                  route->plen,
                  _nm_utils_inet6_ntop(&route->gateway, b2));
        }

        num = nm_ip6_config_get_num_nameservers(priv->ip6_config);
        for (i = 0; i < num; i++) {
            _LOGI("Data:   Internal DNS: %s",
                  _nm_utils_inet6_ntop(nm_ip6_config_get_nameserver(priv->ip6_config, i), b1));
        }

        if (nm_ip6_config_get_num_domains(priv->ip6_config) > 0)
            dns_domain = (char *) nm_ip6_config_get_domain(priv->ip6_config, 0);

        _LOGI("Data:   DNS Domain: '%s'", dns_domain ?: "(none)");
    } else
        _LOGI("Data: No IPv6 configuration");

    if (priv->banner && strlen(priv->banner)) {
        _LOGI("Data: Login Banner:");
        _LOGI("Data: -----------------------------------------");
        _LOGI("Data: %s", priv->banner);
        _LOGI("Data: -----------------------------------------");
    }
}

static void
apply_parent_device_config(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMDevice *   parent_dev      = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(self));
    int          ifindex;
    NMIP4Config *vpn4_parent_config = NULL;
    NMIP6Config *vpn6_parent_config = NULL;

    ifindex = nm_device_get_ip_ifindex(parent_dev);
    if (ifindex > 0) {
        /* If the VPN didn't return a network interface, it is a route-based
         * VPN (like kernel IPSec) and all IP addressing and routing should
         * be done on the parent interface instead.
         */
        if (priv->ip4_config) {
            vpn4_parent_config = nm_ip4_config_new(nm_netns_get_multi_idx(priv->netns), ifindex);
            if (priv->ip_ifindex <= 0)
                nm_ip4_config_merge(vpn4_parent_config,
                                    priv->ip4_config,
                                    NM_IP_CONFIG_MERGE_NO_DNS,
                                    0);
        }
        if (priv->ip6_config) {
            vpn6_parent_config = nm_ip6_config_new(nm_netns_get_multi_idx(priv->netns), ifindex);
            if (priv->ip_ifindex <= 0)
                nm_ip6_config_merge(vpn6_parent_config,
                                    priv->ip6_config,
                                    NM_IP_CONFIG_MERGE_NO_DNS,
                                    0);
        }
    }

    /* Add any explicit route to the VPN gateway through the parent device */
    if (vpn4_parent_config && priv->ip4_external_gw) {
        add_ip4_vpn_gateway_route(vpn4_parent_config,
                                  parent_dev,
                                  priv->ip4_external_gw,
                                  nm_netns_get_platform(priv->netns));
    }
    if (vpn6_parent_config && priv->ip6_external_gw) {
        add_ip6_vpn_gateway_route(vpn6_parent_config,
                                  parent_dev,
                                  priv->ip6_external_gw,
                                  nm_netns_get_platform(priv->netns));
    }

    nm_device_replace_vpn4_config(parent_dev, priv->last_device_ip4_config, vpn4_parent_config);
    g_clear_object(&priv->last_device_ip4_config);
    priv->last_device_ip4_config = vpn4_parent_config;

    nm_device_replace_vpn6_config(parent_dev, priv->last_device_ip6_config, vpn6_parent_config);
    g_clear_object(&priv->last_device_ip6_config);
    priv->last_device_ip6_config = vpn6_parent_config;
}

static gboolean
nm_vpn_connection_apply_config(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    apply_parent_device_config(self);

    if (priv->ip_ifindex > 0) {
        nm_platform_link_set_up(nm_netns_get_platform(priv->netns), priv->ip_ifindex, NULL);

        if (priv->ip4_config) {
            nm_assert(priv->ip_ifindex == nm_ip4_config_get_ifindex(priv->ip4_config));
            if (!nm_ip4_config_commit(priv->ip4_config,
                                      nm_netns_get_platform(priv->netns),
                                      get_route_table(self, AF_INET, FALSE)
                                          ? NM_IP_ROUTE_TABLE_SYNC_MODE_FULL
                                          : NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN))
                return FALSE;
            nm_platform_ip4_dev_route_blacklist_set(nm_netns_get_platform(priv->netns),
                                                    priv->ip_ifindex,
                                                    priv->ip4_dev_route_blacklist);
        }

        if (priv->ip6_config) {
            nm_assert(priv->ip_ifindex == nm_ip6_config_get_ifindex(priv->ip6_config));
            if (!nm_ip6_config_commit(priv->ip6_config,
                                      nm_netns_get_platform(priv->netns),
                                      get_route_table(self, AF_INET6, FALSE)
                                          ? NM_IP_ROUTE_TABLE_SYNC_MODE_FULL
                                          : NM_IP_ROUTE_TABLE_SYNC_MODE_MAIN,
                                      NULL))
                return FALSE;
        }

        if (priv->mtu
            && priv->mtu
                   != nm_platform_link_get_mtu(nm_netns_get_platform(priv->netns),
                                               priv->ip_ifindex))
            nm_platform_link_set_mtu(nm_netns_get_platform(priv->netns),
                                     priv->ip_ifindex,
                                     priv->mtu);
    }

    _LOGI("VPN connection: (IP Config Get) complete");
    if (priv->vpn_state < STATE_PRE_UP)
        _set_vpn_state(self, STATE_PRE_UP, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
    return TRUE;
}

static void
_cleanup_failed_config(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    nm_dbus_object_clear_and_unexport(&priv->ip4_config);
    nm_dbus_object_clear_and_unexport(&priv->ip6_config);

    _LOGW("VPN connection: did not receive valid IP config information");
    _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_IP_CONFIG_INVALID, FALSE);
}

static void
fw_change_zone_cb(NMFirewallManager *      firewall_manager,
                  NMFirewallManagerCallId *call_id,
                  GError *                 error,
                  gpointer                 user_data)
{
    NMVpnConnection *       self = user_data;
    NMVpnConnectionPrivate *priv;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    g_return_if_fail(priv->fw_call == call_id);

    priv->fw_call = NULL;

    if (nm_utils_error_is_cancelled(error))
        return;

    if (error) {
        // FIXME: fail the activation?
    }

    if (!nm_vpn_connection_apply_config(self))
        _cleanup_failed_config(self);
}

static void
nm_vpn_connection_config_maybe_complete(NMVpnConnection *self, gboolean success)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMConnection *          base_con;
    NMSettingConnection *   s_con;
    const char *            zone;

    if (priv->vpn_state < STATE_IP_CONFIG_GET || priv->vpn_state > STATE_ACTIVATED)
        return;

    if (success) {
        if ((priv->has_ip4 && !priv->ip4_config) || (priv->has_ip6 && !priv->ip6_config)) {
            /* Need to wait for other config */
            return;
        }
    }

    nm_clear_g_source(&priv->connect_timeout);

    if (success) {
        print_vpn_config(self);

        /* Add the tunnel interface to the specified firewall zone */
        if (priv->ip_iface) {
            base_con = _get_applied_connection(self);
            s_con    = nm_connection_get_setting_connection(base_con);
            zone     = nm_setting_connection_get_zone(s_con);

            _LOGD("setting firewall zone %s%s%s for '%s'",
                  NM_PRINT_FMT_QUOTED(zone, "'", zone, "'", "(default)"),
                  priv->ip_iface);
            fw_call_cleanup(self);
            priv->fw_call = nm_firewall_manager_add_or_change_zone(nm_firewall_manager_get(),
                                                                   priv->ip_iface,
                                                                   zone,
                                                                   FALSE,
                                                                   fw_change_zone_cb,
                                                                   self);
            return;
        } else if (nm_vpn_connection_apply_config(self))
            return;
    }

    _cleanup_failed_config(self);
}

static gboolean
ip6_addr_from_variant(GVariant *v, struct in6_addr *addr)
{
    const guint8 *bytes;
    gsize         len;

    g_return_val_if_fail(v, FALSE);
    g_return_val_if_fail(addr, FALSE);

    if (g_variant_is_of_type(v, G_VARIANT_TYPE("ay"))) {
        bytes = g_variant_get_fixed_array(v, &len, sizeof(guint8));
        if (len == sizeof(struct in6_addr) && !IN6_IS_ADDR_UNSPECIFIED(bytes)) {
            memcpy(addr, bytes, len);
            return TRUE;
        }
    }
    return FALSE;
}

static struct in6_addr *
ip6_addr_dup_from_variant(GVariant *v)
{
    struct in6_addr *addr;

    addr = g_malloc0(sizeof(*addr));
    if (ip6_addr_from_variant(v, addr))
        return addr;
    g_free(addr);
    return NULL;
}

static gboolean
process_generic_config(NMVpnConnection *self, GVariant *dict)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    const char *            str;
    GVariant *              v;
    guint32                 u32;
    gboolean                b;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CAN_PERSIST, "b", &b) && b) {
        /* Defaults to FALSE, so only let service indicate TRUE */
        priv->service_can_persist = TRUE;
    }

    nm_clear_g_free(&priv->ip_iface);
    priv->ip_ifindex = 0;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_TUNDEV, "&s", &str)) {
        /* Backwards compat with NM-openswan */
        if (g_strcmp0(str, "_none_") != 0)
            priv->ip_iface = g_strdup(str);
    }

    if (priv->ip_iface) {
        /* Grab the interface index for address/routing operations */
        priv->ip_ifindex =
            nm_platform_link_get_ifindex(nm_netns_get_platform(priv->netns), priv->ip_iface);
        if (priv->ip_ifindex <= 0) {
            nm_platform_process_events(nm_netns_get_platform(priv->netns));
            priv->ip_ifindex =
                nm_platform_link_get_ifindex(nm_netns_get_platform(priv->netns), priv->ip_iface);
        }
        if (priv->ip_ifindex <= 0) {
            _LOGE("failed to look up VPN interface index for \"%s\"", priv->ip_iface);
            nm_clear_g_free(&priv->ip_iface);
            priv->ip_ifindex = 0;
            nm_vpn_connection_config_maybe_complete(self, FALSE);
            return FALSE;
        }
    }

    nm_clear_g_free(&priv->banner);
    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_BANNER, "&s", &str)) {
        priv->banner = g_strdup(str);
        _notify(self, PROP_BANNER);
    }

    /* Proxy Config */
    g_clear_object(&priv->proxy_config);
    priv->proxy_config = nm_proxy_config_new();

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_PROXY_PAC, "&s", &str)) {
        nm_proxy_config_set_method(priv->proxy_config, NM_PROXY_CONFIG_METHOD_AUTO);
        nm_proxy_config_set_pac_url(priv->proxy_config, str);
    } else
        nm_proxy_config_set_method(priv->proxy_config, NM_PROXY_CONFIG_METHOD_NONE);

    /* User overrides if any from the NMConnection's Proxy settings */
    nm_proxy_config_merge_setting(priv->proxy_config,
                                  nm_connection_get_setting_proxy(_get_applied_connection(self)));

    /* External world-visible address of the VPN server */
    priv->ip4_external_gw = 0;
    nm_clear_g_free(&priv->ip6_external_gw);

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_EXT_GATEWAY, "u", &u32)) {
        priv->ip4_external_gw = u32;
    } else if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_EXT_GATEWAY, "@ay", &v)) {
        priv->ip6_external_gw = ip6_addr_dup_from_variant(v);
        g_variant_unref(v);

        if (!priv->ip6_external_gw) {
            _LOGE("Invalid IPv6 VPN gateway address received");
            nm_vpn_connection_config_maybe_complete(self, FALSE);
            return FALSE;
        }
    }

    priv->mtu = 0;
    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_MTU, "u", &u32))
        priv->mtu = u32;

    return TRUE;
}

static void
nm_vpn_connection_config_get(NMVpnConnection *self, GVariant *dict)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    gboolean                b;

    g_return_if_fail(dict && g_variant_is_of_type(dict, G_VARIANT_TYPE_VARDICT));

    _LOGI("VPN connection: (IP Config Get) reply received.");

    if (priv->vpn_state == STATE_CONNECT)
        _set_vpn_state(self, STATE_IP_CONFIG_GET, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);

    if (!process_generic_config(self, dict))
        return;

    /* Note whether to expect IPv4 and IPv6 configs */
    priv->has_ip4 = FALSE;
    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_HAS_IP4, "b", &b))
        priv->has_ip4 = b;
    nm_dbus_object_clear_and_unexport(&priv->ip4_config);

    priv->has_ip6 = FALSE;
    if (g_variant_lookup(dict, NM_VPN_PLUGIN_CONFIG_HAS_IP6, "b", &b))
        priv->has_ip6 = b;
    nm_dbus_object_clear_and_unexport(&priv->ip6_config);

    nm_vpn_connection_config_maybe_complete(self, TRUE);
}

guint32
nm_vpn_connection_get_ip4_route_metric(NMVpnConnection *self)
{
    gint64        route_metric;
    NMConnection *applied;

    applied = _get_applied_connection(self);
    route_metric =
        nm_setting_ip_config_get_route_metric(nm_connection_get_setting_ip4_config(applied));

    return (route_metric >= 0) ? route_metric : NM_VPN_ROUTE_METRIC_DEFAULT;
}

guint32
nm_vpn_connection_get_ip6_route_metric(NMVpnConnection *self)
{
    gint64        route_metric;
    NMConnection *applied;

    applied = _get_applied_connection(self);
    route_metric =
        nm_setting_ip_config_get_route_metric(nm_connection_get_setting_ip6_config(applied));

    return (route_metric >= 0) ? route_metric : NM_VPN_ROUTE_METRIC_DEFAULT;
}

static guint32
get_route_table(NMVpnConnection *self, int addr_family, gboolean fallback_main)
{
    NMConnection *     connection;
    NMSettingIPConfig *s_ip;
    guint32            route_table = 0;

    nm_assert(NM_IN_SET(addr_family, AF_INET, AF_INET6));

    connection = _get_applied_connection(self);
    if (connection) {
        s_ip = nm_connection_get_setting_ip_config(connection, addr_family);
        if (s_ip)
            route_table = nm_setting_ip_config_get_route_table(s_ip);
    }

    return route_table ?: (fallback_main ? RT_TABLE_MAIN : 0);
}

static gboolean
_is_device_vrf(NMVpnConnection *self)
{
    NMDevice *parent;
    NMDevice *master;

    parent = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(self));
    if (!parent)
        return FALSE;

    master = nm_device_get_master(parent);
    return master && nm_device_get_link_type(master) == NM_LINK_TYPE_VRF;
}

static void
nm_vpn_connection_ip4_config_get(NMVpnConnection *self, GVariant *dict)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMPlatformIP4Address    address;
    guint32                 u32, route_metric;
    NMSettingIPConfig *     s_ip;
    NMSettingConnection *   s_con;
    guint32                 route_table;
    NMIP4Config *           config;
    GVariantIter *          iter;
    const char *            str;
    GVariant *              v;
    gboolean                b;
    int                     ip_ifindex;
    guint32                 mss           = 0;
    gboolean                never_default = FALSE;

    g_return_if_fail(dict && g_variant_is_of_type(dict, G_VARIANT_TYPE_VARDICT));

    if (priv->vpn_state == STATE_CONNECT)
        _set_vpn_state(self, STATE_IP_CONFIG_GET, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);

    if (priv->vpn_state > STATE_ACTIVATED) {
        _LOGI("VPN connection: (IP4 Config Get) ignoring, the connection is no longer active");
        return;
    }

    if (priv->has_ip4) {
        _LOGI("VPN connection: (IP4 Config Get) reply received");

        if (g_variant_n_children(dict) == 0) {
            priv->has_ip4 = FALSE;
            nm_vpn_connection_config_maybe_complete(self, TRUE);
            return;
        }
    } else {
        _LOGI("VPN connection: (IP4 Config Get) reply received from old-style plugin");

        /* In the old API, the generic and IPv4 configuration items
         * were mixed together.
         */
        if (!process_generic_config(self, dict))
            return;

        priv->has_ip4 = TRUE;
        priv->has_ip6 = FALSE;
    }

    ip_ifindex = nm_vpn_connection_get_ip_ifindex(self, TRUE);
    if (ip_ifindex <= 0)
        g_return_if_reached();

    config = nm_ip4_config_new(nm_netns_get_multi_idx(priv->netns), ip_ifindex);
    nm_ip4_config_set_dns_priority(config, NM_DNS_PRIORITY_DEFAULT_VPN);

    memset(&address, 0, sizeof(address));
    address.plen = 24;

    /* Internal address of the VPN subnet's gateway */
    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_INT_GATEWAY, "u", &u32))
        priv->ip4_internal_gw = u32;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_ADDRESS, "u", &u32))
        address.address = u32;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_PTP, "u", &u32))
        address.peer_address = u32;
    else
        address.peer_address = address.address;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_PREFIX, "u", &u32))
        address.plen = u32;

    if (address.address && address.plen && address.plen <= 32) {
        address.addr_source = NM_IP_CONFIG_SOURCE_VPN;
        nm_ip4_config_add_address(config, &address);
    } else {
        _LOGW("invalid IP4 config received!");
        g_object_unref(config);
        nm_vpn_connection_config_maybe_complete(self, FALSE);
        return;
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_DNS, "au", &iter)) {
        while (g_variant_iter_next(iter, "u", &u32))
            nm_ip4_config_add_nameserver(config, u32);
        g_variant_iter_free(iter);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_NBNS, "au", &iter)) {
        while (g_variant_iter_next(iter, "u", &u32))
            nm_ip4_config_add_wins(config, u32);
        g_variant_iter_free(iter);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_MSS, "u", &u32))
        mss = u32;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_DOMAIN, "&s", &str))
        nm_ip4_config_add_domain(config, str);

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_DOMAINS, "as", &iter)) {
        while (g_variant_iter_next(iter, "&s", &str))
            nm_ip4_config_add_domain(config, str);
        g_variant_iter_free(iter);
    }

    route_table  = get_route_table(self, AF_INET, TRUE);
    route_metric = nm_vpn_connection_get_ip4_route_metric(self);
    s_ip         = nm_connection_get_setting_ip4_config(_get_applied_connection(self));
    s_con        = nm_connection_get_setting_connection(_get_applied_connection(self));

    if (nm_setting_ip_config_get_ignore_auto_routes(s_ip)) {
        /* ignore VPN routes */
    } else if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_PRESERVE_ROUTES, "b", &b) && b) {
        if (priv->ip4_config) {
            NMDedupMultiIter          ipconf_iter;
            const NMPlatformIP4Route *route;

            nm_ip_config_iter_ip4_route_for_each (&ipconf_iter, priv->ip4_config, &route)
                nm_ip4_config_add_route(config, route, NULL);
        }
    } else if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_ROUTES, "aau", &iter)) {
        while (g_variant_iter_next(iter, "@au", &v)) {
            NMPlatformIP4Route route = {
                0,
            };
            guint32 plen;

            switch (g_variant_n_children(v)) {
            case 5:
                g_variant_get_child(v, 4, "u", &route.pref_src);
                /* fall-through */
            case 4:
                g_variant_get_child(v, 0, "u", &route.network);
                g_variant_get_child(v, 1, "u", &plen);
                g_variant_get_child(v, 2, "u", &route.gateway);
                /* 4th item is unused route metric */
                route.table_coerced = nm_platform_route_table_coerce(route_table);
                route.metric        = route_metric;
                route.rt_source     = NM_IP_CONFIG_SOURCE_VPN;

                if (plen > 32 || plen == 0)
                    break;
                route.plen    = plen;
                route.network = nm_utils_ip4_address_clear_host_address(route.network, plen);

                if (priv->ip4_external_gw && route.network == priv->ip4_external_gw
                    && route.plen == 32) {
                    /* Ignore host routes to the VPN gateway since NM adds one itself
                     * below.  Since NM knows more about the routing situation than
                     * the VPN server, we want to use the NM created route instead of
                     * whatever the server provides.
                     */
                    break;
                }

                nm_ip4_config_add_route(config, &route, NULL);
                break;
            default:
                break;
            }
            g_variant_unref(v);
        }
        g_variant_iter_free(iter);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP4_CONFIG_NEVER_DEFAULT, "b", &b))
        never_default = b;

    /* Merge in user overrides from the NMConnection's IPv4 setting */
    nm_ip4_config_merge_setting(config,
                                s_ip,
                                nm_setting_connection_get_mdns(s_con),
                                nm_setting_connection_get_llmnr(s_con),
                                route_table,
                                route_metric);

    if (!never_default && !nm_setting_ip_config_get_never_default(s_ip)) {
        const NMPlatformIP4Route r = {
            .ifindex       = ip_ifindex,
            .rt_source     = NM_IP_CONFIG_SOURCE_VPN,
            .gateway       = priv->ip4_internal_gw,
            .table_coerced = nm_platform_route_table_coerce(route_table),
            .metric        = route_metric,
            .mss           = mss,
        };

        nm_ip4_config_add_route(config, &r, NULL);
    }

    nm_clear_pointer(&priv->ip4_dev_route_blacklist, g_ptr_array_unref);

    nm_ip4_config_add_dependent_routes(config,
                                       route_table,
                                       nm_vpn_connection_get_ip4_route_metric(self),
                                       _is_device_vrf(self),
                                       &priv->ip4_dev_route_blacklist);

    if (priv->ip4_config) {
        nm_ip4_config_replace(priv->ip4_config, config, NULL);
        g_object_unref(config);
    } else {
        priv->ip4_config = config;
        nm_dbus_object_export(NM_DBUS_OBJECT(config));
        g_object_notify((GObject *) self, NM_ACTIVE_CONNECTION_IP4_CONFIG);
    }

    nm_vpn_connection_config_maybe_complete(self, TRUE);
}

static void
nm_vpn_connection_ip6_config_get(NMVpnConnection *self, GVariant *dict)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMPlatformIP6Address    address;
    guint32                 u32, route_metric;
    NMSettingIPConfig *     s_ip;
    guint32                 route_table;
    NMIP6Config *           config;
    GVariantIter *          iter;
    const char *            str;
    GVariant *              v;
    gboolean                b;
    int                     ip_ifindex;
    guint32                 mss           = 0;
    gboolean                never_default = FALSE;

    g_return_if_fail(dict && g_variant_is_of_type(dict, G_VARIANT_TYPE_VARDICT));

    _LOGI("VPN connection: (IP6 Config Get) reply received");

    if (priv->vpn_state == STATE_CONNECT)
        _set_vpn_state(self, STATE_IP_CONFIG_GET, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);

    if (priv->vpn_state > STATE_ACTIVATED) {
        _LOGI("VPN connection: (IP6 Config Get) ignoring, the connection is no longer active");
        return;
    }

    if (g_variant_n_children(dict) == 0) {
        priv->has_ip6 = FALSE;
        nm_vpn_connection_config_maybe_complete(self, TRUE);
        return;
    }

    ip_ifindex = nm_vpn_connection_get_ip_ifindex(self, TRUE);
    if (ip_ifindex <= 0)
        g_return_if_reached();

    config = nm_ip6_config_new(nm_netns_get_multi_idx(priv->netns), ip_ifindex);
    nm_ip6_config_set_dns_priority(config, NM_DNS_PRIORITY_DEFAULT_VPN);

    memset(&address, 0, sizeof(address));
    address.plen = 128;

    /* Internal address of the VPN subnet's gateway */
    nm_clear_g_free(&priv->ip6_internal_gw);
    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_INT_GATEWAY, "@ay", &v)) {
        priv->ip6_internal_gw = ip6_addr_dup_from_variant(v);
        g_variant_unref(v);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_ADDRESS, "@ay", &v)) {
        ip6_addr_from_variant(v, &address.address);
        g_variant_unref(v);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_PTP, "@ay", &v)) {
        ip6_addr_from_variant(v, &address.peer_address);
        g_variant_unref(v);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_PREFIX, "u", &u32))
        address.plen = u32;

    if (!IN6_IS_ADDR_UNSPECIFIED(&address.address) && address.plen && address.plen <= 128) {
        address.addr_source = NM_IP_CONFIG_SOURCE_VPN;
        nm_ip6_config_add_address(config, &address);
    } else {
        _LOGW("invalid IP6 config received!");
        g_object_unref(config);
        nm_vpn_connection_config_maybe_complete(self, FALSE);
        return;
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_DNS, "aay", &iter)) {
        while (g_variant_iter_next(iter, "@ay", &v)) {
            struct in6_addr dns;

            if (ip6_addr_from_variant(v, &dns))
                nm_ip6_config_add_nameserver(config, &dns);
            g_variant_unref(v);
        }
        g_variant_iter_free(iter);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_MSS, "u", &u32))
        mss = u32;

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_DOMAIN, "&s", &str))
        nm_ip6_config_add_domain(config, str);

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_DOMAINS, "as", &iter)) {
        while (g_variant_iter_next(iter, "&s", &str))
            nm_ip6_config_add_domain(config, str);
        g_variant_iter_free(iter);
    }

    route_table  = get_route_table(self, AF_INET6, TRUE);
    route_metric = nm_vpn_connection_get_ip6_route_metric(self);
    s_ip         = nm_connection_get_setting_ip6_config(_get_applied_connection(self));

    if (nm_setting_ip_config_get_ignore_auto_routes(s_ip)) {
        /* Ignore VPN routes */
    } else if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_PRESERVE_ROUTES, "b", &b) && b) {
        if (priv->ip6_config) {
            NMDedupMultiIter          ipconf_iter;
            const NMPlatformIP6Route *route;

            nm_ip_config_iter_ip6_route_for_each (&ipconf_iter, priv->ip6_config, &route)
                nm_ip6_config_add_route(config, route, NULL);
        }
    } else if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_ROUTES, "a(ayuayu)", &iter)) {
        GVariant *dest, *next_hop;
        guint32   prefix, metric;

        while (g_variant_iter_next(iter, "(@ayu@ayu)", &dest, &prefix, &next_hop, &metric)) {
            NMPlatformIP6Route route;

            memset(&route, 0, sizeof(route));

            if (!ip6_addr_from_variant(dest, &route.network))
                goto next;

            if (prefix > 128 || prefix == 0)
                goto next;

            route.plen = prefix;
            ip6_addr_from_variant(next_hop, &route.gateway);
            route.table_coerced = nm_platform_route_table_coerce(route_table);
            route.metric        = route_metric;
            route.rt_source     = NM_IP_CONFIG_SOURCE_VPN;

            nm_utils_ip6_address_clear_host_address(&route.network, &route.network, route.plen);

            if (priv->ip6_external_gw && IN6_ARE_ADDR_EQUAL(&route.network, priv->ip6_external_gw)
                && route.plen == 128) {
                /* Ignore host routes to the VPN gateway since NM adds one itself.
                 * Since NM knows more about the routing situation than the VPN
                 * server, we want to use the NM created route instead of whatever
                 * the server provides.
                 */
                goto next;
            }

            nm_ip6_config_add_route(config, &route, NULL);

next:
            g_variant_unref(dest);
            g_variant_unref(next_hop);
        }
        g_variant_iter_free(iter);
    }

    if (g_variant_lookup(dict, NM_VPN_PLUGIN_IP6_CONFIG_NEVER_DEFAULT, "b", &b))
        never_default = b;

    /* Merge in user overrides from the NMConnection's IPv6 setting */
    nm_ip6_config_merge_setting(config, s_ip, route_table, route_metric);

    if (!never_default && !nm_setting_ip_config_get_never_default(s_ip)) {
        const NMPlatformIP6Route r = {
            .ifindex       = ip_ifindex,
            .rt_source     = NM_IP_CONFIG_SOURCE_VPN,
            .gateway       = *(priv->ip6_internal_gw ?: &in6addr_any),
            .table_coerced = nm_platform_route_table_coerce(route_table),
            .metric        = route_metric,
            .mss           = mss,
        };

        nm_ip6_config_add_route(config, &r, NULL);
    }

    nm_ip6_config_add_dependent_routes(config, route_table, route_metric, _is_device_vrf(self));

    if (priv->ip6_config) {
        nm_ip6_config_replace(priv->ip6_config, config, NULL);
        g_object_unref(config);
    } else {
        priv->ip6_config = config;
        nm_dbus_object_export(NM_DBUS_OBJECT(config));
        g_object_notify((GObject *) self, NM_ACTIVE_CONNECTION_IP6_CONFIG);
    }

    nm_vpn_connection_config_maybe_complete(self, TRUE);
}

static gboolean
connect_timeout_cb(gpointer user_data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    priv->connect_timeout = 0;

    /* Cancel activation if it's taken too long */
    if (priv->vpn_state == STATE_CONNECT || priv->vpn_state == STATE_IP_CONFIG_GET) {
        _LOGW("VPN connection: connect timeout exceeded.");
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_CONNECT_TIMEOUT,
                       FALSE);
    }

    return FALSE;
}

static void
connect_success(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMSettingVpn *          s_vpn;
    guint32                 timeout;

    s_vpn = nm_connection_get_setting_vpn(_get_applied_connection(self));
    g_assert(s_vpn);

    /* Timeout waiting for IP config signal from VPN service
     * It is a configured value or 60 seconds */
    timeout = nm_setting_vpn_get_timeout(s_vpn);
    if (timeout == 0) {
        timeout = nm_config_data_get_connection_default_int64(NM_CONFIG_GET_DATA,
                                                              NM_CON_DEFAULT("vpn.timeout"),
                                                              NULL,
                                                              1,
                                                              G_MAXUINT32,
                                                              60);
    }
    priv->connect_timeout = g_timeout_add_seconds(timeout, connect_timeout_cb, self);

    nm_clear_pointer(&priv->connect_hash, g_variant_unref);
}

static void
connect_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection *self;
    gs_unref_variant GVariant *reply = NULL;
    gs_free_error GError *error      = NULL;

    reply = g_dbus_proxy_call_finish(proxy, result, &error);
    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    self = NM_VPN_CONNECTION(user_data);

    if (error) {
        g_dbus_error_strip_remote_error(error);
        _LOGW("VPN connection: failed to connect: '%s'", error->message);
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
                       FALSE);
    } else
        connect_success(self);
}

static void
connect_interactive_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection *       self;
    NMVpnConnectionPrivate *priv;
    gs_unref_variant GVariant *reply = NULL;
    gs_free_error GError *error      = NULL;

    reply = g_dbus_proxy_call_finish(proxy, result, &error);
    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    self = NM_VPN_CONNECTION(user_data);
    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    _LOGI("VPN connection: (ConnectInteractive) reply received");

    if (g_error_matches(error,
                        NM_VPN_PLUGIN_ERROR,
                        NM_VPN_PLUGIN_ERROR_INTERACTIVE_NOT_SUPPORTED)) {
        _LOGD("VPN connection: falling back to non-interactive connect");

        /* Fall back to Connect() */
        g_dbus_proxy_call(priv->proxy,
                          "Connect",
                          g_variant_new("(@a{sa{sv}})", priv->connect_hash),
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          priv->cancellable,
                          (GAsyncReadyCallback) connect_cb,
                          self);
    } else if (error) {
        g_dbus_error_strip_remote_error(error);
        _LOGW("VPN connection: failed to connect interactively: '%s'", error->message);
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
                       FALSE);
    } else
        connect_success(self);
}

/* Add a username to a hashed connection */
static GVariant *
_hash_with_username(NMConnection *connection, const char *username)
{
    gs_unref_object NMConnection *dup = NULL;
    NMSettingVpn *                s_vpn;

    /* Shortcut if we weren't given a username or if there already was one in
     * the VPN setting; don't bother duplicating the connection and everything.
     */
    s_vpn = nm_connection_get_setting_vpn(connection);
    g_assert(s_vpn);
    if (username == NULL || nm_setting_vpn_get_user_name(s_vpn))
        return nm_connection_to_dbus(connection, NM_CONNECTION_SERIALIZE_ALL);

    dup = nm_simple_connection_new_clone(connection);
    g_assert(dup);
    s_vpn = nm_connection_get_setting_vpn(dup);
    g_assert(s_vpn);
    g_object_set(s_vpn, NM_SETTING_VPN_USER_NAME, username, NULL);
    return nm_connection_to_dbus(dup, NM_CONNECTION_SERIALIZE_ALL);
}

static void
really_activate(NMVpnConnection *self, const char *username)
{
    NMVpnConnectionPrivate *priv;
    GVariantBuilder         details;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    g_return_if_fail(priv->vpn_state == STATE_NEED_AUTH);

    nm_clear_pointer(&priv->connect_hash, g_variant_unref);
    priv->connect_hash = _hash_with_username(_get_applied_connection(self), username);
    g_variant_ref_sink(priv->connect_hash);

    /* If at least one agent doesn't support VPN hints, then we can't use
     * ConnectInteractive(), because that agent won't be able to pass hints
     * from the VPN plugin's interactive secrets requests to the VPN authentication
     * dialog and we won't get the secrets we need.  In this case fall back to
     * the old Connect() call.
     */
    if (nm_agent_manager_all_agents_have_capability(
            nm_agent_manager_get(),
            nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(self)),
            NM_SECRET_AGENT_CAPABILITY_VPN_HINTS)) {
        _LOGD("Allowing interactive secrets as all agents have that capability");

        g_variant_builder_init(&details, G_VARIANT_TYPE_VARDICT);
        g_dbus_proxy_call(priv->proxy,
                          "ConnectInteractive",
                          g_variant_new("(@a{sa{sv}}a{sv})", priv->connect_hash, &details),
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          priv->cancellable,
                          (GAsyncReadyCallback) connect_interactive_cb,
                          self);
    } else {
        _LOGD("Calling old Connect function as not all agents support interactive secrets");
        g_dbus_proxy_call(priv->proxy,
                          "Connect",
                          g_variant_new("(@a{sa{sv}})", priv->connect_hash),
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          priv->cancellable,
                          (GAsyncReadyCallback) connect_cb,
                          self);
    }

    _set_vpn_state(self, STATE_CONNECT, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
}

static void
failure_cb(GDBusProxy *proxy, guint32 reason, gpointer user_data)
{
    NMVpnConnection *self = NM_VPN_CONNECTION(user_data);

    plugin_failed(self, reason);
}

static void
state_changed_cb(GDBusProxy *proxy, guint32 new_service_state, gpointer user_data)
{
    NMVpnConnection *self = NM_VPN_CONNECTION(user_data);

    plugin_state_changed(self, new_service_state);
}

static void
secrets_required_cb(GDBusProxy *       proxy,
                    const char *       message,
                    const char *const *secrets,
                    gpointer           user_data)
{
    NMVpnConnection *self = NM_VPN_CONNECTION(user_data);

    plugin_interactive_secrets_required(self, message, secrets);
}

static void
config_cb(GDBusProxy *proxy, GVariant *dict, gpointer user_data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    /* Only list to this signals during and after connection */
    if (priv->vpn_state >= STATE_NEED_AUTH)
        nm_vpn_connection_config_get(self, dict);
}

static void
ip4_config_cb(GDBusProxy *proxy, GVariant *dict, gpointer user_data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    /* Only list to this signals during and after connection */
    if (priv->vpn_state >= STATE_NEED_AUTH)
        nm_vpn_connection_ip4_config_get(self, dict);
}

static void
ip6_config_cb(GDBusProxy *proxy, GVariant *dict, gpointer user_data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    /* Only list to this signals during and after connection */
    if (priv->vpn_state >= STATE_NEED_AUTH)
        nm_vpn_connection_ip6_config_get(self, dict);
}

static void
_name_owner_changed(GObject *object, GParamSpec *pspec, gpointer user_data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    char *                  owner;

    owner = g_dbus_proxy_get_name_owner(G_DBUS_PROXY(object));

    if (owner && !priv->service_running) {
        /* service appeared */
        priv->service_running = TRUE;
        _LOGI("Saw the service appear; activating connection");

        /* No need to wait for the timeout any longer */
        nm_clear_g_source(&priv->start_timeout);

        /* Expect success because the VPN service has already appeared */
        _nm_dbus_signal_connect(priv->proxy,
                                "Failure",
                                G_VARIANT_TYPE("(u)"),
                                G_CALLBACK(failure_cb),
                                self);
        _nm_dbus_signal_connect(priv->proxy,
                                "StateChanged",
                                G_VARIANT_TYPE("(u)"),
                                G_CALLBACK(state_changed_cb),
                                self);
        _nm_dbus_signal_connect(priv->proxy,
                                "SecretsRequired",
                                G_VARIANT_TYPE("(sas)"),
                                G_CALLBACK(secrets_required_cb),
                                self);
        _nm_dbus_signal_connect(priv->proxy,
                                "Config",
                                G_VARIANT_TYPE("(a{sv})"),
                                G_CALLBACK(config_cb),
                                self);
        _nm_dbus_signal_connect(priv->proxy,
                                "Ip4Config",
                                G_VARIANT_TYPE("(a{sv})"),
                                G_CALLBACK(ip4_config_cb),
                                self);
        _nm_dbus_signal_connect(priv->proxy,
                                "Ip6Config",
                                G_VARIANT_TYPE("(a{sv})"),
                                G_CALLBACK(ip6_config_cb),
                                self);

        _set_vpn_state(self, STATE_NEED_AUTH, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);

        /* Kick off the secrets requests; first we get existing system secrets
         * and ask the plugin if these are sufficient, next we get all existing
         * secrets from system and from user agents and ask the plugin again,
         * and last we ask the user for new secrets if required.
         */
        get_secrets(self, SECRETS_REQ_SYSTEM, NULL);
    } else if (!owner && priv->service_running) {
        /* service went away */
        priv->service_running = FALSE;
        _LOGI("VPN service disappeared");
        nm_vpn_connection_disconnect(self,
                                     NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_STOPPED,
                                     FALSE);
    }

    g_free(owner);
}

static gboolean
_daemon_exec_timeout(gpointer data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(data);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    _LOGW("Timed out waiting for the service to start");
    priv->start_timeout = 0;
    nm_vpn_connection_disconnect(self,
                                 NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_TIMEOUT,
                                 FALSE);
    return G_SOURCE_REMOVE;
}

static int
_get_log_level(void)
{
    NMLogLevel level;

    /* curiously enough, nm-logging also uses syslog. But it
     * maps NMLogLevel differently to the syslog levels then we
     * do here.
     *
     * The reason is, that LOG_NOTICE is already something worth
     * highlighting in the journal, but we have 3 levels that are
     * lower then LOG_NOTICE (LOGL_TRACE, LOGL_DEBUG, LOGL_INFO),
     * On the other hand, syslog only defines LOG_DEBUG and LOG_INFO.
     * Thus, we must map them differently.
     *
     * Inside the VPN plugin, you might want to treat LOG_NOTICE as
     * as low severity, not worthy to be highlighted (like NM does). */

    level = nm_logging_get_level(LOGD_VPN_PLUGIN);
    if (level != _LOGL_OFF) {
        if (level <= LOGL_TRACE)
            return LOG_DEBUG;
        if (level <= LOGL_DEBUG)
            return LOG_INFO;
        if (level <= LOGL_INFO)
            return LOG_NOTICE;
        if (level <= LOGL_WARN)
            return LOG_WARNING;
        if (level <= LOGL_ERR)
            return LOG_ERR;
    }

    return LOG_EMERG;
}

static gboolean
nm_vpn_service_daemon_exec(NMVpnConnection *self, GError **error)
{
    NMVpnConnectionPrivate *priv;
    GPid                    pid;
    char *                  vpn_argv[4];
    gboolean                success     = FALSE;
    GError *                spawn_error = NULL;
    guint                   i, j, n_environ;
    gs_free char **         envp = NULL;
    char                    env_log_level[NM_STRLEN("NM_VPN_LOG_LEVEL=") + 100];
    char                    env_log_syslog[NM_STRLEN("NM_VPN_LOG_SYSLOG=") + 10];
    const int               N_ENVIRON_EXTRA = 3;
    char **                 p_environ;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), FALSE);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    i             = 0;
    vpn_argv[i++] = (char *) nm_vpn_plugin_info_get_program(priv->plugin_info);
    g_return_val_if_fail(vpn_argv[0], FALSE);
    if (nm_vpn_plugin_info_supports_multiple(priv->plugin_info)) {
        vpn_argv[i++] = "--bus-name";
        vpn_argv[i++] = priv->bus_name;
    }
    vpn_argv[i++] = NULL;

    /* we include <unistd.h> and "config.h" defines _GNU_SOURCE for us. So, we have @environ. */
    p_environ = environ;
    n_environ = p_environ ? g_strv_length(p_environ) : 0;
    envp      = g_new(char *, n_environ + N_ENVIRON_EXTRA);
    for (i = 0, j = 0; j < n_environ; j++) {
        if (g_str_has_prefix(p_environ[j], "NM_VPN_LOG_LEVEL=")
            || g_str_has_prefix(p_environ[j], "NM_VPN_LOG_SYSLOG="))
            continue;
        envp[i++] = p_environ[j];
    }

    /* NM_VPN_LOG_LEVEL: the syslog logging level for the plugin. */
    envp[i++] = nm_sprintf_buf(env_log_level, "NM_VPN_LOG_LEVEL=%d", _get_log_level());

    /* NM_VPN_LOG_SYSLOG: whether to log to stdout or syslog. If NetworkManager itself runs in
     * foreground, we also want the plugin to log to stdout.
     * If the plugin runs in background, the plugin should prefer logging to syslog. Otherwise
     * logging messages will be lost (unless using journald, in which case it wouldn't matter). */
    envp[i++] = nm_sprintf_buf(env_log_syslog,
                               "NM_VPN_LOG_SYSLOG=%c",
                               nm_logging_syslog_enabled() ? '1' : '0');

    envp[i++] = NULL;
    nm_assert(i <= n_environ + N_ENVIRON_EXTRA);

    success = g_spawn_async(NULL, vpn_argv, envp, 0, nm_utils_setpgid, NULL, &pid, &spawn_error);

    if (success) {
        _LOGI("Started the VPN service, PID %ld", (long int) pid);
        priv->start_timeout = g_timeout_add_seconds(5, _daemon_exec_timeout, self);
    } else {
        g_set_error(error,
                    NM_MANAGER_ERROR,
                    NM_MANAGER_ERROR_FAILED,
                    "%s",
                    spawn_error ? spawn_error->message : "unknown g_spawn_async() error");

        if (spawn_error)
            g_error_free(spawn_error);
    }

    return success;
}

static void
on_proxy_acquired(GObject *object, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection *       self;
    NMVpnConnectionPrivate *priv;
    gs_free_error GError *error = NULL;
    GDBusProxy *          proxy;

    proxy = g_dbus_proxy_new_for_bus_finish(result, &error);
    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    self = NM_VPN_CONNECTION(user_data);
    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (error) {
        _LOGE("failed to acquire dbus proxy for VPN service: %s", error->message);
        _set_vpn_state(self,
                       STATE_FAILED,
                       NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
                       FALSE);
        return;
    }

    priv->proxy = proxy;

    g_signal_connect(priv->proxy, "notify::g-name-owner", G_CALLBACK(_name_owner_changed), self);
    _name_owner_changed(G_OBJECT(priv->proxy), NULL, self);

    if (priv->service_running)
        return;

    if (!nm_vpn_service_daemon_exec(self, &error)) {
        _LOGW("Could not launch the VPN service. error: %s.", error->message);

        nm_vpn_connection_disconnect(self,
                                     NM_ACTIVE_CONNECTION_STATE_REASON_SERVICE_START_FAILED,
                                     FALSE);
    }
}

void
nm_vpn_connection_activate(NMVpnConnection *self, NMVpnPluginInfo *plugin_info)
{
    NMVpnConnectionPrivate *priv;
    NMSettingVpn *          s_vpn;
    const char *            service;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));
    g_return_if_fail(NM_IS_VPN_PLUGIN_INFO(plugin_info));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    g_return_if_fail(!priv->plugin_info);

    s_vpn = nm_connection_get_setting_vpn(_get_applied_connection(self));
    g_return_if_fail(s_vpn);

    service = nm_vpn_plugin_info_get_service(plugin_info);
    nm_assert(service);

    if (nm_vpn_plugin_info_supports_multiple(plugin_info)) {
        const char *path;

        path = nm_dbus_object_get_path(NM_DBUS_OBJECT(self));
        if (path)
            path = strrchr(path, '/');
        g_return_if_fail(path);

        priv->bus_name = g_strdup_printf("%s.Connection_%s", service, &path[1]);
    } else
        priv->bus_name = g_strdup(service);

    priv->connection_can_persist = nm_setting_vpn_get_persistent(s_vpn);
    priv->plugin_info            = g_object_ref(plugin_info);
    priv->cancellable            = g_cancellable_new();

    g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
                             G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
                             NULL,
                             priv->bus_name,
                             NM_VPN_DBUS_PLUGIN_PATH,
                             NM_VPN_DBUS_PLUGIN_INTERFACE,
                             priv->cancellable,
                             (GAsyncReadyCallback) on_proxy_acquired,
                             self);

    _set_vpn_state(self, STATE_PREPARE, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
}

NMVpnConnectionState
nm_vpn_connection_get_vpn_state(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NM_VPN_CONNECTION_STATE_UNKNOWN);

    return _state_to_nm_vpn_state(NM_VPN_CONNECTION_GET_PRIVATE(self)->vpn_state);
}

const char *
nm_vpn_connection_get_banner(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NULL);

    return NM_VPN_CONNECTION_GET_PRIVATE(self)->banner;
}

NMProxyConfig *
nm_vpn_connection_get_proxy_config(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NULL);

    return NM_VPN_CONNECTION_GET_PRIVATE(self)->proxy_config;
}

NMIP4Config *
nm_vpn_connection_get_ip4_config(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NULL);

    return NM_VPN_CONNECTION_GET_PRIVATE(self)->ip4_config;
}

NMIP6Config *
nm_vpn_connection_get_ip6_config(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NULL);

    return NM_VPN_CONNECTION_GET_PRIVATE(self)->ip6_config;
}

static int
_get_ip_iface_for_device(NMVpnConnection *self, const char **out_iface)
{
    NMDevice *  parent_dev;
    int         ifindex;
    const char *iface;

    nm_assert(NM_IS_VPN_CONNECTION(self));

    /* the ifindex and the ifname in this case should come together.
     * They either must be both set, or none. */

    parent_dev = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(self));
    if (!parent_dev)
        goto none;
    ifindex = nm_device_get_ip_ifindex(parent_dev);
    if (ifindex <= 0)
        goto none;
    iface = nm_device_get_ip_iface(parent_dev);
    if (!iface)
        goto none;

    NM_SET_OUT(out_iface, iface);
    return ifindex;
none:
    NM_SET_OUT(out_iface, NULL);
    return 0;
}

const char *
nm_vpn_connection_get_ip_iface(NMVpnConnection *self, gboolean fallback_device)
{
    NMVpnConnectionPrivate *priv;
    const char *            iface;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), NULL);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->ip_iface || !fallback_device)
        return priv->ip_iface;

    _get_ip_iface_for_device(self, &iface);
    return iface;
}

int
nm_vpn_connection_get_ip_ifindex(NMVpnConnection *self, gboolean fallback_device)
{
    NMVpnConnectionPrivate *priv;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), 0);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->ip_ifindex > 0)
        return priv->ip_ifindex;
    if (!fallback_device)
        return 0;

    return _get_ip_iface_for_device(self, NULL);
}

guint32
nm_vpn_connection_get_ip4_internal_gateway(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), 0);

    return NM_VPN_CONNECTION_GET_PRIVATE(self)->ip4_internal_gw;
}

struct in6_addr *
nm_vpn_connection_get_ip6_internal_gateway(NMVpnConnection *self)
{
    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), 0);

    return NM_VPN_CONNECTION_GET_PRIVATE(self)->ip6_internal_gw;
}

void
nm_vpn_connection_disconnect(NMVpnConnection *             self,
                             NMActiveConnectionStateReason reason,
                             gboolean                      quitting)
{
    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    _set_vpn_state(self, STATE_DISCONNECTED, reason, quitting);
}

gboolean
nm_vpn_connection_deactivate(NMVpnConnection *             self,
                             NMActiveConnectionStateReason reason,
                             gboolean                      quitting)
{
    NMVpnConnectionPrivate *priv;
    gboolean                success = FALSE;

    g_return_val_if_fail(NM_IS_VPN_CONNECTION(self), FALSE);

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);
    if (priv->vpn_state > STATE_UNKNOWN && priv->vpn_state <= STATE_DEACTIVATING) {
        _set_vpn_state(self, STATE_DEACTIVATING, reason, quitting);
        success = TRUE;
    }
    return success;
}

/*****************************************************************************/

static void
plugin_need_secrets_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection *       self;
    NMVpnConnectionPrivate *priv;
    gs_unref_variant GVariant *reply = NULL;
    gs_free_error GError *error      = NULL;
    const char *          setting_name;

    reply = _nm_dbus_proxy_call_finish(proxy, result, G_VARIANT_TYPE("(s)"), &error);
    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    self = NM_VPN_CONNECTION(user_data);
    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (error) {
        g_dbus_error_strip_remote_error(error);
        _LOGE("plugin NeedSecrets request #%d failed: %s", priv->secrets_idx + 1, error->message);
        _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, FALSE);
        return;
    }

    g_variant_get(reply, "(&s)", &setting_name);
    if (!strlen(setting_name)) {
        _LOGD("service indicated no additional secrets required");

        /* No secrets required; we can start the VPN */
        really_activate(self, priv->username);
        return;
    }

    /* More secrets required */
    if (priv->secrets_idx == SECRETS_REQ_NEW) {
        _LOGE("final secrets request failed to provide sufficient secrets");
        _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, FALSE);
    } else {
        _LOGD("service indicated additional secrets required");
        get_secrets(self, priv->secrets_idx + 1, NULL);
    }
}

static void
plugin_new_secrets_cb(GDBusProxy *proxy, GAsyncResult *result, gpointer user_data)
{
    NMVpnConnection *self;
    gs_unref_variant GVariant *reply = NULL;
    gs_free_error GError *error      = NULL;

    reply = g_dbus_proxy_call_finish(proxy, result, &error);
    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    self = NM_VPN_CONNECTION(user_data);

    if (error) {
        g_dbus_error_strip_remote_error(error);
        _LOGE("sending new secrets to the plugin failed: %s", error->message);
        _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, FALSE);
    } else
        _set_vpn_state(self, STATE_CONNECT, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);
}

static void
get_secrets_cb(NMSettingsConnection *      connection,
               NMSettingsConnectionCallId *call_id,
               const char *                agent_username,
               const char *                setting_name,
               GError *                    error,
               gpointer                    user_data)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(user_data);
    NMVpnConnectionPrivate *priv;
    GVariant *              dict;

    g_return_if_fail(NM_IS_VPN_CONNECTION(self));

    priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    g_return_if_fail(connection && connection == _get_settings_connection(self, FALSE));
    g_return_if_fail(call_id == priv->secrets_id);

    priv->secrets_id = NULL;

    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    if (error && priv->secrets_idx >= SECRETS_REQ_NEW) {
        _LOGE("Failed to request VPN secrets #%d: %s", priv->secrets_idx + 1, error->message);
        _set_vpn_state(self, STATE_FAILED, NM_ACTIVE_CONNECTION_STATE_REASON_NO_SECRETS, FALSE);
        return;
    }

    /* Cache the username for later */
    if (agent_username) {
        g_free(priv->username);
        priv->username = g_strdup(agent_username);
    }

    dict = _hash_with_username(_get_applied_connection(self), priv->username);

    if (priv->secrets_idx == SECRETS_REQ_INTERACTIVE) {
        _LOGD("sending secrets to the plugin");

        /* Send the secrets back to the plugin */
        g_dbus_proxy_call(priv->proxy,
                          "NewSecrets",
                          g_variant_new("(@a{sa{sv}})", dict),
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          priv->cancellable,
                          (GAsyncReadyCallback) plugin_new_secrets_cb,
                          self);
    } else {
        _LOGD("asking service if additional secrets are required");

        /* Ask the VPN service if more secrets are required */
        g_dbus_proxy_call(priv->proxy,
                          "NeedSecrets",
                          g_variant_new("(@a{sa{sv}})", dict),
                          G_DBUS_CALL_FLAGS_NONE,
                          -1,
                          priv->cancellable,
                          (GAsyncReadyCallback) plugin_need_secrets_cb,
                          self);
    }
}

static void
get_secrets(NMVpnConnection *self, SecretsReq secrets_idx, const char *const *hints)
{
    NMVpnConnectionPrivate *     priv  = NM_VPN_CONNECTION_GET_PRIVATE(self);
    NMSecretAgentGetSecretsFlags flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE;

    g_return_if_fail(secrets_idx < SECRETS_REQ_LAST);
    priv->secrets_idx = secrets_idx;

    cancel_get_secrets(self);

    _LOGD("requesting VPN secrets pass #%d", priv->secrets_idx + 1);

    switch (priv->secrets_idx) {
    case SECRETS_REQ_SYSTEM:
        flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ONLY_SYSTEM;
        break;
    case SECRETS_REQ_EXISTING:
        flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE;
        break;
    case SECRETS_REQ_NEW:
    case SECRETS_REQ_INTERACTIVE:
        flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
        break;
    default:
        g_assert_not_reached();
    }

    if (nm_active_connection_get_user_requested(NM_ACTIVE_CONNECTION(self)))
        flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_USER_REQUESTED;

    priv->secrets_id = nm_settings_connection_get_secrets(
        _get_settings_connection(self, FALSE),
        _get_applied_connection(self),
        nm_active_connection_get_subject(NM_ACTIVE_CONNECTION(self)),
        NM_SETTING_VPN_SETTING_NAME,
        flags,
        hints,
        get_secrets_cb,
        self);
    g_return_if_fail(priv->secrets_id);
}

static void
plugin_interactive_secrets_required(NMVpnConnection *  self,
                                    const char *       message,
                                    const char *const *secrets)
{
    NMVpnConnectionPrivate *priv        = NM_VPN_CONNECTION_GET_PRIVATE(self);
    const gsize             secrets_len = NM_PTRARRAY_LEN(secrets);
    gsize                   i;
    gs_free const char **   hints        = NULL;
    gs_free char *          message_hint = NULL;

    if (!NM_IN_SET(priv->vpn_state, STATE_CONNECT, STATE_NEED_AUTH)) {
        _LOGD("VPN plugin: requested secrets; state %s (%d); ignore request in current state",
              vpn_state_to_string_a(priv->vpn_state),
              priv->vpn_state);
        return;
    }

    _LOGI("VPN plugin: requested secrets; state %s (%d)",
          vpn_state_to_string_a(priv->vpn_state),
          priv->vpn_state);

    priv->secrets_idx = SECRETS_REQ_INTERACTIVE;
    _set_vpn_state(self, STATE_NEED_AUTH, NM_ACTIVE_CONNECTION_STATE_REASON_NONE, FALSE);

    /* Copy hints and add message to the end */
    hints = g_new(const char *, secrets_len + 2);
    for (i = 0; i < secrets_len; i++)
        hints[i] = secrets[i];
    if (message) {
        message_hint = g_strdup_printf("x-vpn-message:%s", message);
        hints[i++]   = message_hint;
    }
    hints[i] = NULL;
    nm_assert(i < secrets_len + 2);

    get_secrets(self, SECRETS_REQ_INTERACTIVE, hints);
}

/*****************************************************************************/

static void
device_changed(NMActiveConnection *active, NMDevice *new_device, NMDevice *old_device)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(active);

    if (!_service_and_connection_can_persist(NM_VPN_CONNECTION(active)))
        return;
    if (priv->vpn_state < STATE_CONNECT || priv->vpn_state > STATE_ACTIVATED)
        return;

    /* Route-based VPNs must update their routing and send a new IP config
     * since all their routes need to be adjusted for new_device.
     */
    if (priv->ip_ifindex <= 0)
        return;

    /* Device changed underneath the VPN connection.  Let the plugin figure
     * out that connectivity is down and start its reconnect attempt if it
     * needs to.
     */
    if (old_device)
        remove_parent_device_config(NM_VPN_CONNECTION(active), old_device);

    if (new_device)
        apply_parent_device_config(NM_VPN_CONNECTION(active));
}

/*****************************************************************************/

static void
nm_vpn_connection_init(NMVpnConnection *self)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    priv->vpn_state   = STATE_WAITING;
    priv->secrets_idx = SECRETS_REQ_SYSTEM;
    priv->netns       = g_object_ref(nm_netns_get());
}

static void
dispose(GObject *object)
{
    NMVpnConnection *       self = NM_VPN_CONNECTION(object);
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(self);

    if (priv->proxy)
        g_signal_handlers_disconnect_by_data(priv->proxy, self);

    nm_clear_g_source(&priv->start_timeout);

    nm_clear_pointer(&priv->connect_hash, g_variant_unref);

    nm_clear_pointer(&priv->ip4_dev_route_blacklist, g_ptr_array_unref);

    nm_clear_g_source(&priv->connect_timeout);

    dispatcher_cleanup(self);

    cancel_get_secrets(self);

    nm_clear_g_cancellable(&priv->cancellable);

    g_clear_object(&priv->proxy_config);
    nm_dbus_object_clear_and_unexport(&priv->ip4_config);
    nm_dbus_object_clear_and_unexport(&priv->ip6_config);
    g_clear_object(&priv->proxy);
    g_clear_object(&priv->plugin_info);

    fw_call_cleanup(self);

    nm_pacrunner_manager_remove_clear(&priv->pacrunner_conf_id);

    G_OBJECT_CLASS(nm_vpn_connection_parent_class)->dispose(object);
}

static void
finalize(GObject *object)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(object);

    g_free(priv->banner);
    g_free(priv->ip_iface);
    g_free(priv->username);
    g_free(priv->ip6_internal_gw);
    g_free(priv->ip6_external_gw);

    G_OBJECT_CLASS(nm_vpn_connection_parent_class)->finalize(object);

    g_clear_object(&priv->netns);
}

static gboolean
ip_config_valid(VpnState state)
{
    return (state == STATE_PRE_UP || state == STATE_ACTIVATED);
}

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMVpnConnectionPrivate *priv = NM_VPN_CONNECTION_GET_PRIVATE(object);
    NMDevice *              parent_dev;

    switch (prop_id) {
    case PROP_VPN_STATE:
        g_value_set_uint(value, _state_to_nm_vpn_state(priv->vpn_state));
        break;
    case PROP_BANNER:
        g_value_set_string(value, priv->banner ?: "");
        break;
    case PROP_IP4_CONFIG:
        nm_dbus_utils_g_value_set_object_path(value,
                                              ip_config_valid(priv->vpn_state) ? priv->ip4_config
                                                                               : NULL);
        break;
    case PROP_IP6_CONFIG:
        nm_dbus_utils_g_value_set_object_path(value,
                                              ip_config_valid(priv->vpn_state) ? priv->ip6_config
                                                                               : NULL);
        break;
    case PROP_MASTER:
        parent_dev = nm_active_connection_get_device(NM_ACTIVE_CONNECTION(object));
        nm_dbus_utils_g_value_set_object_path(value, parent_dev);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static const GDBusSignalInfo signal_info_vpn_state_changed = NM_DEFINE_GDBUS_SIGNAL_INFO_INIT(
    "VpnStateChanged",
    .args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("state", "u"),
                                      NM_DEFINE_GDBUS_ARG_INFO("reason", "u"), ), );

static const NMDBusInterfaceInfoExtended interface_info_vpn_connection = {
    .parent = NM_DEFINE_GDBUS_INTERFACE_INFO_INIT(
        NM_DBUS_INTERFACE_VPN_CONNECTION,
        .signals    = NM_DEFINE_GDBUS_SIGNAL_INFOS(&nm_signal_info_property_changed_legacy,
                                                &signal_info_vpn_state_changed, ),
        .properties = NM_DEFINE_GDBUS_PROPERTY_INFOS(
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("VpnState",
                                                             "u",
                                                             NM_VPN_CONNECTION_VPN_STATE),
            NM_DEFINE_DBUS_PROPERTY_INFO_EXTENDED_READABLE_L("Banner",
                                                             "s",
                                                             NM_VPN_CONNECTION_BANNER), ), ),
    .legacy_property_changed = TRUE,
};

static void
nm_vpn_connection_class_init(NMVpnConnectionClass *connection_class)
{
    GObjectClass *           object_class      = G_OBJECT_CLASS(connection_class);
    NMActiveConnectionClass *active_class      = NM_ACTIVE_CONNECTION_CLASS(connection_class);
    NMDBusObjectClass *      dbus_object_class = NM_DBUS_OBJECT_CLASS(connection_class);

    dbus_object_class->interface_infos = NM_DBUS_INTERFACE_INFOS(&interface_info_vpn_connection);

    object_class->get_property = get_property;
    object_class->dispose      = dispose;
    object_class->finalize     = finalize;

    active_class->device_state_changed = device_state_changed;
    active_class->device_changed       = device_changed;

    obj_properties[PROP_VPN_STATE] = g_param_spec_uint(NM_VPN_CONNECTION_VPN_STATE,
                                                       "",
                                                       "",
                                                       NM_VPN_CONNECTION_STATE_UNKNOWN,
                                                       NM_VPN_CONNECTION_STATE_DISCONNECTED,
                                                       NM_VPN_CONNECTION_STATE_UNKNOWN,
                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_BANNER] = g_param_spec_string(NM_VPN_CONNECTION_BANNER,
                                                      "",
                                                      "",
                                                      NULL,
                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);

    g_object_class_override_property(object_class, PROP_MASTER, NM_ACTIVE_CONNECTION_MASTER);
    g_object_class_override_property(object_class,
                                     PROP_IP4_CONFIG,
                                     NM_ACTIVE_CONNECTION_IP4_CONFIG);
    g_object_class_override_property(object_class,
                                     PROP_IP6_CONFIG,
                                     NM_ACTIVE_CONNECTION_IP6_CONFIG);

    signals[INTERNAL_STATE_CHANGED] = g_signal_new(NM_VPN_CONNECTION_INTERNAL_STATE_CHANGED,
                                                   G_OBJECT_CLASS_TYPE(object_class),
                                                   G_SIGNAL_RUN_FIRST,
                                                   0,
                                                   NULL,
                                                   NULL,
                                                   NULL,
                                                   G_TYPE_NONE,
                                                   3,
                                                   G_TYPE_UINT,
                                                   G_TYPE_UINT,
                                                   G_TYPE_UINT);

    signals[INTERNAL_RETRY_AFTER_FAILURE] =
        g_signal_new(NM_VPN_CONNECTION_INTERNAL_RETRY_AFTER_FAILURE,
                     G_OBJECT_CLASS_TYPE(object_class),
                     G_SIGNAL_RUN_FIRST,
                     0,
                     NULL,
                     NULL,
                     NULL,
                     G_TYPE_NONE,
                     0);
}
